namespace N {
	export const a = function (this: typeof N, n: 5) {
		return n;
	};
	export const b = function (this: void, n: 5) {
		return n;
	};
	export const c = function (n: 5) {
		return n;
	};
	export const d = (n: 5) => {
		return n;
	};
	export function e(this: typeof N, n: 5) {
		return n;
	}
	export function f(this: void, n: 5) {
		return n;
	}
	export function g(n: 5) {
		return n;
	}
}

export = () => {
	it("should support function declarations", () => {
		function foo() {
			return true;
		}
		expect(foo()).to.be.ok();
	});

	it("should support function arguments", () => {
		function add(a: number, b: number) {
			return a + b;
		}
		expect(add(123, 456)).to.equal(579);
	});

	it("should support no return value", () => {
		function doStuff(doIHaveToMom: boolean) {
			if (doIHaveToMom) {
				return "cleans room";
			} else {
				return;
			}
		}

		expect(doStuff(true)).to.equal("cleans room");
		expect(doStuff(false)).never.to.be.ok();
	});

	it("should support destructuring in assignment", () => {
		function addAndMultiply(a: number, b: number): [number, number] {
			const sum = a + b;
			const product = a * b;
			return [sum, product];
		}

		const [x, y] = addAndMultiply(5, 6);

		expect(x).to.equal(11);
		expect(y).to.equal(30);
	});

	it("should support variadic arguments", () => {
		function addAll(...n: Array<number>) {
			let sum = 0;
			for (const value of n) {
				sum += value;
			}
			return sum;
		}
		expect(addAll()).to.equal(0);
		expect(addAll(1)).to.equal(1);
		expect(addAll(1, 2, 3)).to.equal(6);
	});

	it("should support default arguments", () => {
		function addSeven(n = 5) {
			return n + 7;
		}
		expect(addSeven()).to.equal(12);
		expect(addSeven(7)).to.equal(14);
	});

	it("should support function expressions", () => {
		expect(
			(function () {
				return 123;
			})(),
		).to.equal(123);
	});

	it("should support arrow functions", () => {
		expect(
			(() => {
				return 456;
			})(),
		).to.equal(456);
	});

	it("should support binding patterns", () => {
		function foo({ a }: { a: number }) {
			return a * 2;
		}
		expect(foo({ a: 4 })).to.equal(8);

		function bar([a, b]: [number, number]) {
			return a * b;
		}
		expect(bar([4, 7])).to.equal(28);
	});

	it("should support this parameter", () => {
		function foo(this: void, x: number, y: number, z: number) {
			return `${x}, ${y}, ${z}`;
		}
		expect(foo(1, 2, 3)).to.equal("1, 2, 3");
	});

	it("should properly define and call methods vs callbacks", () => {
		class A {
			static a(this: void, n: 5) {
				expect(this).to.equal(undefined);
				expect(n).to.equal(5);
			}
			static b(this: typeof A, n: 5) {
				expect(this).to.equal(A);
				expect(n).to.equal(5);
			}
			static c(n: 5) {
				expect(n).to.equal(5);
			}
			public d(this: void, n: 5) {
				expect(this).to.equal(undefined);
				expect(n).to.equal(5);
			}
			public e = function (n: 5) {
				expect(n).to.equal(5);
			};
			public f = function (this: A, n: 5) {
				expect(this instanceof A).to.equal(true);
				expect(n).to.equal(5);
			};
			public g = (n: 5) => {
				expect(n).to.equal(5);
			};
			public h(this: this, n: 5) {
				expect(this instanceof A).to.equal(true);
				expect(n).to.equal(5);
			}
		}

		const o = {
			a(this: void, n: 5) {
				expect(this).to.equal(undefined);
				expect(n).to.equal(5);
			},
			b(n: 5) {
				expect(this).to.equal(o);
				expect(n).to.equal(5);
			},
			c: function (this: void, n: 5) {
				expect(this).to.equal(undefined);
				expect(n).to.equal(5);
			},
			d: function (n: 5) {
				expect(this).to.equal(o);
				expect(n).to.equal(5);
			},
			e: (n: 5) => {
				expect(n).to.equal(5);
			},
			f(this: {}, n: 5) {
				expect(this).to.equal(o);
				expect(n).to.equal(5);
			},
			g: function (this: {}, n: 5) {
				expect(this).to.equal(o);
				expect(n).to.equal(5);
			},
		};

		function f(this: void, n: 5) {
			// expect(this).to.equal(undefined);
			expect(n).to.equal(5);
		}

		const g = function (this: void, n: 5) {
			// expect(this).to.equal(undefined);
			expect(n).to.equal(5);
		};

		A.a(5);
		A.b(5);
		A.c(5);
		const a = new A();
		a.d(5);
		a.e(5);
		a.f(5);
		a.g(5);
		a.h(5);

		o.a(5);
		o.b(5);
		o.c(5);
		o.d(5);
		o.e(5);
		o.f(5);
		o.g(5);

		expect(N.a(5)).to.equal(5);
		expect(N.b(5)).to.equal(5);
		expect(N.c(5)).to.equal(5);
		expect(N.d(5)).to.equal(5);
		expect(N.e(5)).to.equal(5);
		expect(N.f(5)).to.equal(5);
		expect(N.g(5)).to.equal(5);

		f(5);
		g(5);

		(function (this: void, n: 5) {
			expect(n).to.equal(5);
		})(5);
		({
			x: function (n: 5) {
				expect(n).to.equal(5);
			},
		}).x(5);

		({
			x: function (this: void, n: 5) {
				expect(n).to.equal(5);
			},
		}).x(5);

		({
			x: function (this: {}, n: 5) {
				expect(n).to.equal(5);
			},
		}).x(5);

		((n: 5) => {
			expect(n).to.equal(5);
		})(5);

		const obj = {
			saferNum: function (this: void, n: number): number {
				return n === n && n > 0 ? n : 0;
			}, // wouldn't compile previously because it is a named function
		};
		const { saferNum } = obj;
		expect(saferNum(5)).to.equal(5);
	});

	it("should optimize array spread paramters", () => {
		function foo(...[a, b, c]: [number, string, boolean]) {
			return [a, b, c];
		}
		const result = foo(123, "abc", true);
		expect(result[0]).to.equal(123);
		expect(result[1]).to.equal("abc");
		expect(result[2]).to.equal(true);
	});

	it("should protect against void values passed into built-in functions", () => {
		function foo() {}
		expect(tonumber(foo())).to.equal(undefined);
	});

	it("should not wrap argument spread expressions in parentheses", () => {
		const args: unknown[] = ["123", "456"];
		expect(select(1, ...args)[1]).to.equal("456");
	});

	it("should support spreading into a macro with optional arguments", () => {
		const array = [1, 2, 3, 4, 5];
		const args = [(value: number, accum: number) => value + accum, 10] as const;
		const sum = array.reduce(...args);
		expect(sum).to.equal(25);
	});
};
